今天大概會聊到的範圍
- CompositionLocal
- CompositionLocalProvider
在上一篇研究 MaterialTheme
的時候,我們知道要使用 theme 內的顏色時,只需要使用 MaterialTheme.colors.xxx
就好了。但這個魔法是怎麼發生的呢?
object MaterialTheme {
val colors: Colors
@Composable
@ReadOnlyComposable
get() = LocalColors.current
val typography: Typography
@Composable
@ReadOnlyComposable
get() = LocalTypography.current
val shapes: Shapes
@Composable
@ReadOnlyComposable
get() = LocalShapes.current
}
可以看到,在 MaterialTheme
中,其實個 property 都各只是一個 getter function,其實他們分別是 LocalColors
、LocalShapes
和 LocalTypography
。
咦?這個命名規則好像在哪裡看過?
val context = LocalContext.current
val owner = LocalLifecycleOwner.current
仔細一看,發現這些 current 都為了實作同一個 property -- CompositionLocal.current
在 Compose 的結構中,Composable 要將某個資料傳遞給 child composable 時,通常會透過 state 參數往下傳遞。但是有時,某個比較裡層的 composable 需要某個資料,但不想依賴中間每一層的 composable 都帶著這個資料時,可以怎麼處理呢?
CompositionLocal
就是一個 "隱式的" 資料存放方式,在 CompositionLocal
所控制的 Scope 以下的樹狀結構,都可以取得同一組 CompositionLocal
與其中的資料。
@Composable
fun Screen() {
MaterialTheme {
// ... 這裡可能有很多 composable
SomeComp()
}
}
@Composable
fun SomeComp(){
SomeInnerComp()
}
@Composable
fun SomeInnerComp() {
val color = MaterialTheme.color.primaryColor // 即便傳了好幾層還是可以取得 color
}
讓我們再次看看 MaterialTheme
的 source code
@Composable
fun MaterialTheme(
colors: Colors = MaterialTheme.colors,
typography: Typography = MaterialTheme.typography,
shapes: Shapes = MaterialTheme.shapes,
content: @Composable () -> Unit
) {
val rememberedColors = remember {
// Explicitly creating a new object here so we don't mutate the initial [colors]
// provided, and overwrite the values set in it.
colors.copy()
}.apply { updateColorsFrom(colors) }
val rippleIndication = rememberRipple()
val selectionColors = rememberTextSelectionColors(rememberedColors)
CompositionLocalProvider( // <--- 1.
LocalColors provides rememberedColors, // <--- 2.
LocalContentAlpha provides ContentAlpha.high,
LocalIndication provides rippleIndication,
LocalRippleTheme provides MaterialRippleTheme,
LocalShapes provides shapes,
LocalTextSelectionColors provides selectionColors,
LocalTypography provides typography
) { // <--- 3.
ProvideTextStyle(value = typography.body1, content = content)
}
}
MaterialTheme
中,建立了一個 CompositionLocalProvider
[1]。在建立 CompositionLocalProvider
時,會需要透過 "provides" 將資料設定到 LocalXXX 內 [2]。這些 LocalXXX 各自都是自己一個 ProvidableCompositionLocal
(繼承於 CompositionLocal
)。ProvidableCompositionLocal
繼承於 CompositionLocal
,並且可以透過 provides function 提供資料。最後在提供要包在這個 CompositionLocalProvider
中的 content [3]。
MaterialTheme
中包的 content 是一個 ProvideTextStyle
。ProvideTextStyle
中又有另一個 CompositionLocalProvider
:
@Composable
fun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) {
val mergedStyle = LocalTextStyle.current.merge(value)
CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content)
}
這裡要提到另一個概念,CompositionLocal
是有範圍性的。CompositionLocalProvider
所包裝的 content 與其中所包中的每一層 composable 都可以取得對應的 LocalXXX。
CompositionLocal
)建立 CompositionLocal
的方法有兩個:compositionLocalOf()
和 stataicCompositionLocalOf()
。
val LocalData = compositionLocalOf {
// factory fucntion to create initial data
}
產生完 LocalData 後,再建立 CompositionLocalProvider
並提供資料
CompositionLocalProvider(
LocalData provides <data>
) {
UIComposable()
}
在 provides 資料時,可以 provide 一般資料之外,也可以提供 state。當 provides 時提供的 state 改變時,就會觸發 recompistion。
compositionLocalOf
:當資料改變時,有使用到該 LocalXXX 的 Composable 會觸發 recompositionstataicCompositionLocalOf
:整個 CompositionLocal
所包覆的 composable 和整個 compose tree 都會被 recomposeCompositionLocalProvider
除了可以將資料放進自定義的 CompositionLocal
之外,也可以用來覆寫上層 CompistionLocalProvider
所提供的參數。
@Composable
fun CompositionLocalExample() {
MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
Column {
Text("Uses MaterialTheme's provided alpha")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("Medium value provided for LocalContentAlpha")
Text("This Text also uses the medium value")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
DescendantExample()
}
}
}
}
}
compose 官方文件所提供的範例
雖然 CompositionLocal
可以很方便的提供資料給好幾層之外的 Composable 資料,但同時也很容易被濫用。因為 CompositionLocal
是隱性的傳遞,無法明確知道資料的設定方與取得方分別在哪。所以在使用 CompositionLocal
時可以停看聽:
CompositionLocal
是否有預設值?是否一定會有值被寫入?CompositionLocal
是否真的可能被整個 View Tree 中的“任何”一個人用到?如果不符合,也許可以選用別的傳遞方式。
官方文件有提反面例子:我們是否能用 CompositionLocal
存放 ViewModel
?
我的第一直覺是可以,因為 ViewModel
的確會影響到整個 tree 的 UI。
但是仔細想想,其實不是每一個按鈕、每一個 Text 都需要知道 ViewModel
與其中的各種資料,所以透過 CompostionLocal
將 ViewModel
存放起來並不是一個好方法。
取而代之,可以考慮將資料透過參數傳遞,但提供 default 值:
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
// ...
MyDescendant(myViewModel.data)
}
發現 CompositionLocal
其實很常在大大小小的地方出現,只是通常都是別人準備好的 CompostionLocal
。實際研究後發現它不僅是一個好用的工具,同時也是一個非常重要且有趣的主題!
Reference: